/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package digitaldreamers.libraries.games.servers.networkmanagement.concrete;

import digitaldreamers.libraries.games.servers.networkmanagement.abstracts.AbPacketHandler;
import digitaldreamers.libraries.games.servers.networkmanagement.abstracts.IConnectionHandler;
import digitaldreamers.libraries.games.servers.networkmanagement.abstracts.IGameServer;
import digitaldreamers.libraries.games.servers.networkmanagement.abstracts.IPlayerComponent;
import digitaldreamers.libraries.games.servers.networkmanagement.packets.sending.AbSendablePacket;
import digitaldreamers.libraries.games.servers.networkmanagement.packets.sending.SCheckConnectionPacket;
import java.io.*;
import java.net.Socket;
import java.util.ArrayList;

/**
 * This class handles the network connection between player and game server.
 *
 * @author DaoLe
 */
class ConnectionHandler implements IConnectionHandler, Runnable {
    /**
     * This class is used to check connection state.
     */
    private static class CheckConnectionThread extends Thread {
        
        private static final String MESSAGE__THREAD_ENDED = "%s ended";
        private static final String CONNECTION_CHECKING_THREAD_NAME = "Connection checking thread";
        private static final int SLEEP_TIME = 5000;
        
        private ConnectionHandler mConnectionHandler;
        private SCheckConnectionPacket mCheckConnectionPacket;
        private volatile boolean mIsRunning;

        /**
         * Create a new instance.
         *
         * @param pConnectionHandler The ConnectionHandler object that is used to
         * send check data.
         * @throws IOException Error that occurred while creating a dummy packet.
         */
        public CheckConnectionThread(ConnectionHandler pConnectionHandler) throws IOException {
            mConnectionHandler = pConnectionHandler;
            mIsRunning = false;
            mCheckConnectionPacket = new SCheckConnectionPacket();
        }

        /**
         * Main process.
         */
        @Override
        public void run() {
            while (mIsRunning) {
                try {
                    mConnectionHandler.send(mCheckConnectionPacket);
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException ex) {
                    mConnectionHandler.showMessage(ex.getMessage());
                }
            }
            System.out.println(String.format(MESSAGE__THREAD_ENDED, CONNECTION_CHECKING_THREAD_NAME));
        }

        /**
         * Start the connection check thread.
         * @throws IllegalThreadStateException Something wrong happened.
         */
        public void startThread() throws IllegalThreadStateException {
            try {
                if(!mIsRunning) {
                    mIsRunning = true;
                    start();
                }
            } catch (IllegalThreadStateException ex) {
                throw ex;
            }
        }

        /**
         * Stop the connection check thread.
         * @throws InterruptedException Something wrong happened.
         */
        public void stopThread() throws InterruptedException {
            try {
                if(mIsRunning) {
                    mIsRunning = false;
                    join();
                }
            } catch (InterruptedException ex) {
                throw ex;
            }
        }
    }
    
    private static final String MESSAGE__THREAD_ENDED = "%s ended";
    private static final String CONNECTION_HANDLER_THREAD_NAME = "Connection handling thread";
    private static final String ERROR_MESSAGE__INVALID_PACKET_LENGTH = "Packet length must be greater than 0 and less than 1025.";
    private static final String ERROR_MESSAGE__DATA_READING = "Could not read packet data.";
    private static final int SLEEP_TIME = 100;
    private static final int PACKET_LENGTH_BYTES = 4;
    private static final int MAX_PACKET_LENGTH_BYTES = 1024;
    
    // Listeners on the connection error event.
    private ArrayList<IOnConnectionErrorListener> mConnectionErrorListeners;
    
    private IPlayerComponent mPlayer;
    private IGameServer mGameServer;
    private AbPacketHandler mPacketHandler;
    private Socket mSocket;
    private InputStream mInputStream;
    private DataOutputStream mDataOutputStream;
    private Thread mReceiveDataThread;
    private CheckConnectionThread mCheckConnectionThread;
    private ConnectionStates mState;
    private volatile boolean mIsRunning;
    private boolean mConnectionCheckEnable;
    
    /**
     * Get the game server.
     * 
     * @return The game server.
     */
    @Override
    public IGameServer getGameServer() {
        return mGameServer;
    }

    /**
     * Get the player of this connection.
     *
     * @return The player of this connection.
     */
    @Override
    public IPlayerComponent getPlayer() {
        return mPlayer;
    }

    /**
     * Set the pPlayer of this connection.
     *
     * @param pPlayer The pPlayer of this connection.
     */
    @Override
    public void setPlayer(IPlayerComponent pPlayer) {
        mPlayer = pPlayer;
        if (pPlayer == null) {
            fireConnectionErrorEvent();
        }
    }
    
    /**
     * 
     * @param pSocket The pSocket.
     * @param pGameServer The game server.
     * @param pPacketHandler The packet handler for this connection handler.
     */
    public final void setInitialState(Socket pSocket, IGameServer pGameServer, AbPacketHandler pPacketHandler, boolean pConnectionCheckEnable) {
        mSocket = pSocket;
        mGameServer = pGameServer;
        mPacketHandler = pPacketHandler;
        mPacketHandler.setConnectionHandler(this);
        mConnectionCheckEnable = pConnectionCheckEnable;
        mIsRunning = false;
        mConnectionErrorListeners.clear();
        mState = ConnectionStates.INITIAL;
        try {
            mInputStream = pSocket.getInputStream();
            mDataOutputStream = new DataOutputStream(new BufferedOutputStream(pSocket.getOutputStream()));
        } catch (IOException ex) {
            showMessage(ex.getMessage());
            fireConnectionErrorEvent();
        }
    }
    
    /**
     * Create a new connection handler.
     *
     * @param pSocket The pSocket.
     * @param pGameServer The game server.
     * @param pPacketHandler The packet handler.
     */
    public ConnectionHandler(Socket pSocket, IGameServer pGameServer, AbPacketHandler pPacketHandler, boolean pConnectionCheckEnable) {
        mConnectionErrorListeners = new ArrayList<>();
        setInitialState(pSocket, pGameServer, pPacketHandler, pConnectionCheckEnable);
    }

    /**
     * Main process of this connection thread.
     */
    @Override
    public void run() {
        mState = ConnectionStates.RUNNING;
        while (mIsRunning) {
            try {
                if (mSocket.getInputStream().available() > 0) {
                    byte[] data = receiveFixedLengthData(mInputStream, PACKET_LENGTH_BYTES);
                    if (data == null) {
                        throw new Exception(ERROR_MESSAGE__DATA_READING);
                    }
                    // Get message length.
                    ByteArrayInputStream BAIS = new ByteArrayInputStream(data);
                    DataInputStream DIS = new DataInputStream(BAIS);
                    int packetLength = DIS.readInt();

                    // Get the data.
                    if (packetLength > 0) {
                        data = receiveFixedLengthData(mInputStream, packetLength);
                        if (data == null) {
                            throw new Exception(ERROR_MESSAGE__DATA_READING);
                        }
                    }
                    mPacketHandler.addPacket(data);
                }
            } catch (IllegalArgumentException ex) {
                showMessage(ex.getMessage());
                fireConnectionErrorEvent();
            } catch (Exception ex) {
                showMessage(ex.getMessage());
                fireConnectionErrorEvent();
            }
            try {
                Thread.sleep(SLEEP_TIME);
            } catch (InterruptedException ex) {
                showMessage(ex.getMessage());
            }
        }
        System.out.println(String.format(MESSAGE__THREAD_ENDED, CONNECTION_HANDLER_THREAD_NAME));
    }

    /**
     * Receive the data sent from client.
     *
     * @param pInputStream The input stream of this connection.
     * @param pCount The number of bytes to read.
     * @return The data sent from client.
     * @throws IllegalArgumentException Error thrown when the packet length is invalid.
     * @throws Exception Error that occurred while reading data.
     */
    private byte[] receiveFixedLengthData(InputStream pInputStream, int pCount) throws IllegalArgumentException, Exception {
        // Check the data.
        if (pCount <= 0 || pCount > MAX_PACKET_LENGTH_BYTES) {
            throw new IllegalArgumentException(ERROR_MESSAGE__INVALID_PACKET_LENGTH);
        }

        // Set the buffer with the input length.
        byte[] buffer = new byte[pCount];
        int receivedCount;
        int position = 0;

        // Read until all data is received.
        do {
            // Read the stream to buffer.
            receivedCount = pInputStream.read(buffer, position, pCount - position);
            if (receivedCount == 0) {
                throw new Exception(ERROR_MESSAGE__DATA_READING);
            }
            position += receivedCount;
        } while (position < pCount);
        return buffer;
    }

    /**
     * Send data to client.
     *
     * @param pSendablePacket The data to send.
     */
    @Override
    public void send(AbSendablePacket pSendablePacket) {
        try {
            byte[] data = pSendablePacket.toByteArray();
            
            // Send the length of the data first.
            mDataOutputStream.write(convertIntToByteArray(data.length));
            mDataOutputStream.flush();

            // Then send the real data.
            mDataOutputStream.write(data);
            mDataOutputStream.flush();
        } catch (IOException ex) {
            showMessage(ex.getMessage());
            
            // Raise connection eror event.
            fireConnectionErrorEvent();
        } catch (Exception ex) {
            showMessage(ex.getMessage());
            
            // Raise connection eror event.
            fireConnectionErrorEvent();
        }
    }
    
    /**
     * Start this connection thread.
     */
    @Override
    public void start() {
        try {
            if(!mIsRunning) {
                mState = ConnectionStates.STARTED;
                mIsRunning = true;
                mPacketHandler.start();
                mReceiveDataThread = new Thread(this);
                mReceiveDataThread.start();
                if(mConnectionCheckEnable && mCheckConnectionThread == null) {
                    mCheckConnectionThread = new CheckConnectionThread(this);
                    mCheckConnectionThread.startThread();
                }
            }
        } catch(IllegalThreadStateException ex) {
            showMessage(ex.getMessage());
            fireConnectionErrorEvent();
        } catch (IOException ex) {
            showMessage(ex.getMessage());
            fireConnectionErrorEvent();
        }
    }
    
    /**
     * Stop this connection thread.
     */
    @Override
    public void stop() {
        try {
            if(mIsRunning) {
                mState = ConnectionStates.STOPPED;

                // Stop all the threads, close all the streams and socket.
                if(mConnectionCheckEnable && mCheckConnectionThread != null) {
                    mCheckConnectionThread.stopThread();
                    mCheckConnectionThread = null;
                }
                mIsRunning = false;
                mReceiveDataThread.join();
                mPacketHandler.stop();
                mInputStream.close();
                mDataOutputStream.close();
                mSocket.close();
                ConnectionHandlerFactory.releaseConnectionHandler(this);
            }
        } catch (InterruptedException ex) {
            showMessage(ex.getMessage());
            fireConnectionErrorEvent();
        } catch (IOException ex) {
            showMessage(ex.getMessage());
            fireConnectionErrorEvent();
        } catch (Exception ex) {
            showMessage(ex.getMessage());
            fireConnectionErrorEvent();
        }
    }
    
    @Override
    public void showMessage(String pMessage) {
        mGameServer.showMessage(pMessage);
    }
    
    /**
     * Convert integer to byte array.
     *
     * @param pValue The value to convert.
     * @return The converted byte array.
     */
    private byte[] convertIntToByteArray(int pValue) {
        return new byte[] {
            (byte) (pValue >>> 24),
            (byte) (pValue >>> 16),
            (byte) (pValue >>> 8),
            (byte) pValue
        };
    }

    /**
     * Register a new pListener for the connection error event.
     *
     * @param pListener The new pListener of the event.
     * @return The result whether or not the pListener is registered.
     */
    @Override
    public boolean addConnectionErrorListener(IOnConnectionErrorListener pListener) {
        
        // Only register new listeners.
        if (!mConnectionErrorListeners.contains(pListener)) {
            return mConnectionErrorListeners.add(pListener);
        }
        return false;
    }

    /**
     * Unregister a pListener for the connection error event.
     *
     * @param pListener The registered pListener of the event.
     * @return The result whether or not the pListener is unregistered.
     */
    @Override
    public boolean removeConnectionErrorListener(IOnConnectionErrorListener pListener) {
        
        // Only remove registered pListener.
        if (mConnectionErrorListeners.contains(pListener)) {
            return mConnectionErrorListeners.remove(pListener);
        }
        return false;
    }

    /**
     * Fire the connection error event.
     *
     * @param connectionHandler The ConnectionHandler object that raised the
     * event.
     */
    @Override
    public void fireConnectionErrorEvent() {
        IOnConnectionErrorListener[] listeners = new IOnConnectionErrorListener[mConnectionErrorListeners.size()];
        listeners = mConnectionErrorListeners.toArray(listeners);
        for (IOnConnectionErrorListener listener : listeners) {
            listener.onConnectionError(this, mState);
        }
    }
}